"use strict";

require("../__util__/test-init");

const fs = require("fs");
const path = require("path");
const expect = require("chai").expect;
const toDiffableHtml = require("diffable-html");
const marko = require("marko");
const autotest = require("mocha-autotest").default;
const createBrowserWithMarko = require("../__util__/create-marko-jsdom-module");
const domToString = require("../__util__/domToString");
const browser = createBrowserWithMarko(__dirname, "<html><body></body></html>");

autotest("fixtures", {
  html: testRunner,
  vdom: testRunner,
  "html ≅ vdom": compareNormalized,
});

autotest("fixtures-async-callback", {
  html: testRunner,
});

function testRunner(fixture) {
  fixture.test(() => runRenderTest(fixture));
}

function compareNormalized({ test, context }) {
  test(function () {
    if (!("html" in context) || !("vdom" in context)) {
      this.skip();
    } else {
      expect(context.html).to.equal(context.vdom);
    }
  });
}

async function runRenderTest(fixture) {
  let dir = fixture.dir;
  let output = fixture.mode || "html";
  let snapshot = fixture.snapshot;
  let isVDOM = output === "vdom";

  browser.error = undefined;

  let templatePath = path.join(dir, "template.marko");
  let mainPath = path.join(dir, "test.js");
  let main = !fs.existsSync(mainPath)
    ? {}
    : isVDOM
      ? browser.require(mainPath)
      : require(mainPath);
  let loadOptions = main && main.loadOptions;

  try {
    var compilerOptions = {
      output: output,
      writeToDisk: main.writeToDisk !== false,
      preserveWhitespace: main.preserveWhitespaceGlobal === true,
      ignoreUnrecognizedTags: main.ignoreUnrecognizedTags === true,
    };

    require("marko/compiler").configure(compilerOptions);

    if (main.checkError) {
      let e;

      try {
        let template = isVDOM
          ? browser.require(templatePath)
          : marko.load(templatePath, loadOptions);
        let templateData = Object.assign({}, main.templateData || {});

        if (template.default) {
          template = template.default;
        }

        await template.render(templateData);
      } catch (_e) {
        e = _e;
        let errorFile = path.join(dir, "error.txt");
        fs.writeFileSync(errorFile, e.stack.toString(), {
          encoding: "utf8",
        });
      }

      if (!e) {
        throw new Error("Error expected");
      }

      main.checkError(e);
      return;
    } else {
      let template = isVDOM
        ? browser.require(templatePath)
        : marko.load(templatePath, loadOptions);
      let templateData = Object.assign({}, main.templateData || {});

      if (template.default) {
        template = template.default;
      }

      let html = "";
      let out = isVDOM
        ? template.createOut()
        : template.createOut(
            {},
            {
              write: (data) => (html += data),
              flush: () => {
                if (!main.noFlushComment) {
                  html += "<!--FLUSH-->";
                }
              },
              end: () => {
                html = html.replace(/<!--FLUSH-->$/, "");
                out.emit("finish");
              },
            },
          );
      let asyncEventsVerifier = createAsyncVerifier(
        main,
        snapshot,
        out,
        isVDOM,
      );

      if (main.sync) {
        out.sync();
      }

      await template.render(templateData, out).end();

      if (isVDOM) {
        let document = browser.window.document;
        let actualNode = document.createDocumentFragment();
        out.___getResult().replaceChildrenOf(actualNode);

        actualNode.normalize();
        let vdomString = domToString(actualNode, {
          childrenOnly: true,
        });

        snapshot(vdomString, {
          name: "vdom",
          ext: ".html",
        });

        (fixture.context || (fixture.context = {})).vdom =
          normalizeHtml(actualNode);

        if (browser.error) {
          const err = browser.error;
          browser.error = undefined;
          throw err;
        }
      } else {
        if (main.checkHtml) {
          fs.writeFileSync(path.join(dir, "actual.html"), html, {
            encoding: "utf8",
          });
          main.checkHtml(html);
        } else {
          snapshot(html, {
            ext: ".html",
            format: toDiffableHtml,
          });
        }

        (fixture.context || (fixture.context = {})).html = normalizeHtml(html);
      }

      asyncEventsVerifier.verify();
    }
  } finally {
    require("marko/compiler").configure();
  }
}

function normalizeHtml(htmlOrNode) {
  let document = browser.window.document;

  if (typeof htmlOrNode === "string") {
    document.open();
    document.write(htmlOrNode);
    document.close();
  } else {
    document.documentElement.innerHTML = "";
    document.body.appendChild(htmlOrNode);
  }

  const treeWalker = document.createTreeWalker(document.body);
  const nodesToRemove = [];

  while (treeWalker.nextNode()) {
    const node = treeWalker.currentNode;
    if (
      node.nodeType === 8 ||
      isIgnoredTag(node) ||
      isClientReorderFragment(node)
    ) {
      nodesToRemove.push(node);
    }
    if (node.nodeType === 1) {
      if (node.tagName === "TEXTAREA") {
        node.textContent = node.value;
      }

      // sort attrs by name.
      Array.from(node.attributes)
        .sort((a, b) => a.name.localeCompare(b.name))
        .forEach((attr) => {
          node.removeAttributeNode(attr);
          node.setAttributeNode(attr);
        });
    }
  }

  nodesToRemove.forEach((n) => n.remove());
  document.body.innerHTML += "";
  document.body.normalize();

  return document.body.innerHTML.trim();
}

function isIgnoredTag(node) {
  switch (node.tagName) {
    case "LINK":
    case "TITLE":
    case "STYLE":
    case "SCRIPT":
      return true;
    default:
      return false;
  }
}

function isClientReorderFragment(node) {
  return /^af\d+$/.test(node.id);
}

function createAsyncVerifier(main, snapshot, out, isVDOM) {
  var events = [];
  var eventsByAwaitInstance = {};

  var addEventListener = function (event) {
    out.on(event, function (arg) {
      var name = arg.name;

      if (!eventsByAwaitInstance[name]) {
        eventsByAwaitInstance[name] = [];
      }

      eventsByAwaitInstance[name].push(event);

      events.push({
        event: event,
        arg: Object.assign({}, arg),
      });
    });
  };

  addEventListener("await:begin");
  addEventListener("await:beforeRender");
  addEventListener("await:finish");

  return {
    verify() {
      if (main.checkEvents && !isVDOM) {
        main.checkEvents(events, snapshot, out);
      }

      // Make sure all of the await instances were correctly ended
      Object.keys(eventsByAwaitInstance).forEach(function (name) {
        var events = eventsByAwaitInstance[name];
        expect(events).to.deep.equal([
          "await:begin",
          "await:beforeRender",
          "await:finish",
        ]);
      });
    },
  };
}
